package wxz.test.charset;

import java.nio.CharBuffer;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.util.Map;
import java.util.Iterator;
import java.io.Writer;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileReader;

/**
 * A Charset implementation which performs Rot13 encoding. Rot-13 encoding is a
 * simple text obfuscation algorithm which shifts alphabetical characters by 13
 * so that 'a' becomes 'n', 'o' becomes 'b', etc. This algorithm was popularized
 * by the Usenet discussion forums many years ago to mask naughty words, hide
 * answers to questions, and so on. The Rot13 algorithm is symmetrical, applying
 * it to text that has been scrambled by Rot13 will give you the original
 * unscrambled text.
 * 
 * Applying this Charset encoding to an output stream will cause everything you
 * write to that stream to be Rot13 scrambled as it's written out. And appying
 * it to an input stream causes data read to be Rot13 descrambled as it's read.
 * 
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class Rot13Charset extends Charset {
    // the name of the base charset encoding we delegate to
    private static final String BASE_CHARSET_NAME = "UTF-8";
    // Handle to the real charset we'll use for transcoding between
    // characters and bytes. Doing this allows us to apply the Rot13
    // algorithm to multibyte charset encodings. But only the
    // ASCII alpha chars will be rotated, regardless of the base encoding.
    Charset baseCharset;

    /**
     * Constructor for the Rot13 charset. Call the superclass constructor to
     * pass along the name(s) we'll be known by. Then save a reference to the
     * delegate Charset.
     */
    protected Rot13Charset(String canonical, String[] aliases) {
        super(canonical, aliases);
        // Save the base charset we're delegating to
        baseCharset = Charset.forName(BASE_CHARSET_NAME);
    }

    // ----------------------------------------------------------
    /**
     * Called by users of this Charset to obtain an encoder. This implementation
     * instantiates an instance of a private class (defined below) and passes it
     * an encoder from the base Charset.
     */
    public CharsetEncoder newEncoder() {
        return new Rot13Encoder(this, baseCharset.newEncoder());
    }

    /**
     * Called by users of this Charset to obtain a decoder. This implementation
     * instantiates an instance of a private class (defined below) and passes it
     * a decoder from the base Charset.
     */
    public CharsetDecoder newDecoder() {
        return new Rot13Decoder(this, baseCharset.newDecoder());
    }

    /**
     * This method must be implemented by concrete Charsets. We always say no,
     * which is safe.
     */
    public boolean contains(Charset cs) {
        return (false);
    }

    /**
     * Common routine to rotate all the ASCII alpha chars in the given
     * CharBuffer by 13. Note that this code explicitly compares for upper and
     * lower case ASCII chars rather than using the methods
     * Character.isLowerCase and Character.isUpperCase. This is because the
     * rotate-by-13 scheme only works properly for the alphabetic characters of
     * the ASCII charset and those methods can return true for non-ASCII Unicode
     * chars.
     */
    private void rot13(CharBuffer cb) {
        for (int pos = cb.position(); pos < cb.limit(); pos++) {
            char c = cb.get(pos);
            char a = '\u0000';
            // Is it lowercase alpha?
            if ((c >= 'a') && (c <= 'z')) {
                a = 'a';
            }
            // Is it uppercase alpha?
            if ((c >= 'A') && (c <= 'Z')) {
                a = 'A';
            }
            // If either, roll it by 13
            if (a != '\u0000') {
                c = (char) ((((c - a) + 13) % 26) + a);
                cb.put(pos, c);
            }
        }
    }

    // --------------------------------------------------------
    /**
     * The encoder implementation for the Rot13 Charset. This class, and the
     * matching decoder class below, should also override the "impl" methods,
     * such as implOnMalformedInput( ) and make passthrough calls to the
     * baseEncoder object. That is left as an exercise for the hacker.
     */
    private class Rot13Encoder extends CharsetEncoder {
        private CharsetEncoder baseEncoder;

        /**
         * Constructor, call the superclass constructor with the Charset object
         * and the encodings sizes from the delegate encoder.
         */
        Rot13Encoder(Charset cs, CharsetEncoder baseEncoder) {
            super(cs, baseEncoder.averageBytesPerChar(), baseEncoder.maxBytesPerChar());
            this.baseEncoder = baseEncoder;
        }

        /**
         * Implementation of the encoding loop. First, we apply the Rot13
         * scrambling algorithm to the CharBuffer, then reset the encoder for
         * the base Charset and call it's encode( ) method to do the actual
         * encoding. This may not work properly for non-Latin charsets. The
         * CharBuffer passed in may be read-only or re-used by the caller for
         * other purposes so we duplicate it and apply the Rot13 encoding to the
         * copy. We DO want to advance the position of the input buffer to
         * reflect the chars consumed.
         */
        protected CoderResult encodeLoop(CharBuffer cb, ByteBuffer bb) {
            CharBuffer tmpcb = CharBuffer.allocate(cb.remaining());
            while (cb.hasRemaining()) {
                tmpcb.put(cb.get());
            }
            tmpcb.rewind();
            rot13(tmpcb);
            baseEncoder.reset();
            CoderResult cr = baseEncoder.encode(tmpcb, bb, true);
            // If error or output overflow, we need to adjust
            // the position of the input buffer to match what
            // was really consumed from the temp buffer. If
            // underflow (all input consumed), this is a no-op.
            cb.position(cb.position() - tmpcb.remaining());
            return (cr);
        }
    }

    // --------------------------------------------------------
    /**
     * The decoder implementation for the Rot13 Charset.
     */
    private class Rot13Decoder extends CharsetDecoder {
        private CharsetDecoder baseDecoder;

        /**
         * Constructor, call the superclass constructor with the Charset object
         * and pass alon the chars/byte values from the delegate decoder.
         */
        Rot13Decoder(Charset cs, CharsetDecoder baseDecoder) {
            super(cs, baseDecoder.averageCharsPerByte(), baseDecoder.maxCharsPerByte());
            this.baseDecoder = baseDecoder;
        }

        /**
         * Implementation of the decoding loop. First, we reset the decoder for
         * the base charset, then call it to decode the bytes into characters,
         * saving the result code. The CharBuffer is then de-scrambled with the
         * Rot13 algorithm and the result code is returned. This may not work
         * properly for non-Latin charsets.
         */
        protected CoderResult decodeLoop(ByteBuffer bb, CharBuffer cb) {
            baseDecoder.reset();
            CoderResult result = baseDecoder.decode(bb, cb, true);
            rot13(cb);
            return (result);
        }
    }

    // --------------------------------------------------------
    /**
     * Unit test for the Rot13 Charset. This main( ) will open and read an input
     * file if named on the command line, or stdin if no args are provided, and
     * write the contents to stdout via the X-ROT13 charset encoding. The
     * "encryption" implemented by the Rot13 algorithm is symmetrical. Feeding
     * in a plain-text file, such as Java source code for example, will output a
     * scrambled version. Feeding the scrambled version back in will yield the
     * original plain-text document.
     */
    public static void main(String[] argv) throws Exception {
        BufferedReader in;
        if (argv.length > 0) {
            // Open the named file
            in = new BufferedReader(new FileReader(argv[0]));
        } else {
            // Wrap a BufferedReader around stdin
            in = new BufferedReader(new InputStreamReader(System.in));
        }
        // Create a PrintStream that uses the Rot13 encoding
        PrintStream out = new PrintStream(System.out, false, "X-ROT13");
        String s = null;
        // Read all input and write it to the output.
        // As the data passes through the PrintStream,
        // it will be Rot13-encoded.
        while ((s = in.readLine()) != null) {
            out.println(s);
        }
        out.flush();
    }
}